﻿namespace NJQuery {
	using System;
	using System.Collections.Specialized;
	using System.Web.UI;
	using System.Web.UI.WebControls;
	using System.ComponentModel;
	using System.Globalization;
	using System.Diagnostics.CodeAnalysis;

	/// <summary>
	/// Server control used to generate a <see href="http://jqueryui.com/demos/datepicker/">jQuery UI Datepicker widget</see> as a dropdown on an input box.
	/// </summary>
	public class DatePicker : DatePickerBase {

		// TODO: Serialize/Deserialize DayNames*, MonthNames* properties to ViewState

		#region Constants: Property defaults

		private const bool defaultButtonImageOnly = false;
		private const string defaultButtonText = "...";
		private const string defaultCloseText = "Close";
		private const bool defaultConstrainInput = true;
		private const string defaultDurationString = "Normal";
		private const bool defaultGoToCurrent = false;
		private const string defaultShortYearCutoffString = "+10";
		private const string defaultShowAnimString = "show";
		private const DropDownShowOn defaultShowOn = DropDownShowOn.Focus;

		#endregion


		#region Static Fields: Property defaults

		//If changing these values, be sure to change the corresponding string constant to match
		private static readonly Duration defaultDuration = NJQuery.Duration.Normal;
		private static readonly RelativeValue defaultShortYearCutoff = new RelativeValue(true, 10);
		private static readonly DatePickerAnimation defaultShowAnim = DatePickerAnimation.Show;

		#endregion


		#region Static Methods: Private routines

		private static string GetText(DateTime? date) {
			if(date.HasValue) return date.Value.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture);
			return string.Empty;
		}

		private static DateTime? ParseDate(string text) {
			DateTime value;
			if(DateTime.TryParse(text, out value)) return value.Date;
			return null;
		}

		#endregion


		#region Fields

		private IEffect showAnimControl;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="DatePicker"/> class.
		/// </summary>
		public DatePicker() : base(HtmlTextWriterTag.Input) { }

		#endregion


		#region Properties: Widget initialization options

		/// <summary>
		/// The text to display after each date field, e.g. to show the required format.
		/// This property is HTML encoded and mapped to the "appendText" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(""), Category("Appearance")]
		public virtual string AppendText {
			get { return ViewState["AppendText"] as string ?? string.Empty; }
			set { ViewState["AppendText"] = value; }
		}

		/// <summary>
		/// The URL for the popup button image. If set, <see cref="ButtonText"/> becomes the alt value and is not directly displayed.
		/// This property performs a <see cref="O:System.Web.UI.Page.ResolveUrl()"/> before mapping to the "buttonImage" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <seealso cref="ButtonText"/>
		/// <seealso cref="ButtonImageOnly"/>
		[DefaultValue(""), UrlProperty, Category("Appearance")]
		public virtual string ButtonImage {
			get { return ViewState["ButtonImage"] as string ?? string.Empty; }
			set { ViewState["ButtonImage"] = value; }
		}

		/// <summary>
		/// Set to true to place an image after the field to use as the trigger without it appearing on a button.
		/// This property maps directly to the "buttonImageOnly" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <seealso cref="ButtonText"/>
		/// <seealso cref="ButtonImage"/>
		[DefaultValue(defaultButtonImageOnly), Category("Appearance")]
		public virtual bool ButtonImageOnly {
			get { return ViewState["ButtonImageOnly"] as bool? ?? defaultButtonImageOnly; }
			set { ViewState["ButtonImageOnly"] = value; }
		}

		/// <summary>
		/// The text to display on the trigger button. Use in conjunction with <see cref="ShowOn"/> equal to <see cref="DropDownShowOn.Button"/> or <see cref="DropDownShowOn.Both"/>.
		/// This property is HTML encoded (if <see cref="ButtonImage"/> not provided) and mapped directly to the "buttonText" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <seealso cref="ButtonImage"/>
		/// <seealso cref="ButtonImageOnly"/>
		/// <seealso cref="ShowOn"/>
		[DefaultValue(defaultButtonText), Category("Appearance")]
		public virtual string ButtonText {
			get { return ViewState["ButtonText"] as string ?? defaultButtonText; }
			set { ViewState["ButtonText"] = value; }
		}

		/// <summary>
		/// The text to display for the close link. This attribute is one of the regionalisation attributes.
		/// This property is HTML encoded and mapped to the "closeText" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultCloseText), Category("Appearance")]
		public virtual string CloseText {
			get { return ViewState["CloseText"] as string ?? defaultCloseText; }
			set { ViewState["CloseText"] = value; }
		}

		/// <summary>
		/// Whether or not the user input will be constrained.
		/// This property maps directly to the "constrainInput" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultConstrainInput), Category("Behavior")]
		public virtual bool ConstrainInput {
			get { return ViewState["ConstrainInput"] as bool? ?? defaultConstrainInput; }
			set { ViewState["ConstrainInput"] = value; }
		}

		//TODO: Bridge .NET to JavaScript DateFormat
		//[DefaultValue(defaultDateFormat), Category("Appearance")]
		//public virtual string DateFormat {
		//	get { return ViewState["DateFormat"] as string ?? defaultDateFormat; }
		//	set { ViewState["DateFormat"] = value; }
		//}

		/// <summary>
		/// Set the date to display on first opening if the field is blank.
		/// This property is mapped to the "defaultDate" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(null), Category("Behavior")]
		public virtual DateTime? DefaultDate {
			get { return ViewState["DefaultDate"] as DateTime?; }
			set { ViewState["DefaultDate"] = value; }
		}

		/// <summary>
		/// Controls the speed at which the datepicker appears.  This is a named speed ("Normal", "Fast" or "Slow") a number of milliseconds or immediate.
		/// This property is mapped to the "duration" option when the jQuery UI widget is initialized unless overridden by an Effect server control set in the ShowAdmin property.
		/// </summary>
		/// <seealso cref="ShowAnim"/>
		[DefaultValue(typeof(Duration), defaultDurationString), Category("Behavior")]
		public virtual Duration Duration {
			get { return ViewState["Duration"] as Duration? ?? defaultDuration; }
			set { ViewState["Duration"] = value; }
		}

		/// <summary>
		/// If true, the current day link moves to the currently selected date instead of today.
		/// This property maps directly to the "gotoCurrent" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultGoToCurrent), Category("Behavior")]
		public virtual bool GoToCurrent {
			get { return ViewState["GoToCurrent"] as bool? ?? defaultGoToCurrent; }
			set { ViewState["GoToCurrent"] = value; }
		}

		/// <summary>
		/// Sets the cutoff year for determining the century for a date (used in conjunction with DateFormat 'y'). If an unsigned numeric value through 99 is provided then this value is used directly. If a the value is preceded with a + or -, then it is converted to a number and added to the current year. Once the cutoff year is calculated, any dates entered with a year value less than or equal to it are considered to be in the current century, while those greater than it are deemed to be in the previous century.
		/// This property maps directly to the "shortYearCutoff" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">Relative value must be between -99 and 99, inclusive.</exception>
		[DefaultValue(typeof(RelativeValue), defaultShortYearCutoffString), Category("Behavior")]
		public virtual RelativeValue ShortYearCutoff {
			get { return ViewState["ShortYearCutoff"] as RelativeValue? ?? defaultShortYearCutoff; }
			set {
				if(value.Relative && (value.Value < -99 || value.Value > 99)) throw new ArgumentOutOfRangeException("value", value, "Relative value must be between -99 and 99, inclusive.");
				ViewState["ShortYearCutoff"] = value;
			}
		}

		/// <summary>
		/// Set the name of the animation used to show/hide the datepicker.
		/// This property maps to the "showAnim" using either the standard Datepicker widget animation name or an effect defined by a referenced Effect server control.  If the animation references an effect, the <see cref="Duration"/> property will be overriden and the "showOptions" option may be also set when the jQuery UI widget is initialized.
		/// </summary>
		/// <seealso cref="Duration"/>
		[DefaultValue(typeof(DatePickerAnimation), defaultShowAnimString), Category("Behavior")]
		[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Anim", Justification = "Exposed as the jQuery option name.")]
		public virtual DatePickerAnimation ShowAnim {
			get { return ViewState["ShowAnim"] as DatePickerAnimation? ?? defaultShowAnim; }
			set { ViewState["ShowAnim"] = value; }
		}

		/// <summary>
		/// Have the datepicker appear automatically when the field receives focus (<see cref="DropDownShowOn.Focus"/>), appear only when a button is clicked (<see cref="DropDownShowOn.Button"/>) or appear when either event takes place (<see cref="DropDownShowOn.Both"/>).
		/// This property maps directly to the "showOn" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <exception cref="ArgumentException">Value not defined.</exception>
		[DefaultValue(defaultShowOn), Category("Behavior")]
		public virtual DropDownShowOn ShowOn {
			get { return ViewState["ShowOn"] as DropDownShowOn? ?? defaultShowOn; }
			set {
				if(!Enum.IsDefined(typeof(DropDownShowOn), value)) throw new ArgumentException("Value not defined.", "value");
				ViewState["ShowOn"] = value;
			}
		}

		/// <summary>
		/// Allows you to define your own event when the datepicker is closed, whether or not a date is selected. The function receives the selected date as a Date and the datepicker instance as parameters. "this" refers to the associated input field.
		/// This is a client-side JavaScript event with the signature function(dateText,inst).
		/// This property maps directly to the "onClose" option when the jQuery UI widget is initialized.
		/// </summary>
		public virtual string OnClientClose {
			get { return ViewState["OnClientClose"] as string ?? string.Empty; }
			set { ViewState["OnClientClose"] = value; }
		}

		#endregion


		#region Properties: Additional

		/// <summary>
		/// Gets or set whether the user is able to modify the value in the input control.
		/// </summary>
		[DefaultValue(false), Category("Behavior")]
		[SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "ReadOnly", Justification = "Matches property used by System.Web.UI.WebControls.TextBox.")]
		public virtual bool ReadOnly {
			get { return ViewState["ReadOnly"] as bool? ?? false; }
			set { ViewState["ReadOnly"] = value; }
		}

		#endregion


		#region Methods: Rendering hooks

		/// <summary>
		/// Raises the <see cref="Control.PreRender"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected override void OnPreRender(EventArgs e) {
			base.OnPreRender(e);

			DatePickerAnimation showAnim = ShowAnim;

			string showAnimControlId = ShowAnim.ControlId;
			if(!string.IsNullOrEmpty(showAnimControlId)) showAnimControl = NamingContainer.FindControl(showAnimControlId) as IEffect;
		}

		/// <summary>
		/// Adds HTML attributes and styles that need to be rendered to the specified <see cref="HtmlTextWriter"/>.
		/// This property is used primarily by control developers.
		/// </summary>
		/// <param name="writer">The <see cref="HtmlTextWriter"/> object that receives the rendered content.</param>
		protected override void AddAttributesToRender(HtmlTextWriter writer) {

			Page.ClientScript.RegisterForEventValidation(UniqueID);

			writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);
			writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
			writer.AddAttribute(HtmlTextWriterAttribute.Value, GetText(SelectedDate));

			base.AddAttributesToRender(writer);

			if(ReadOnly) writer.AddAttribute(HtmlTextWriterAttribute.ReadOnly, "readonly");
			writer.AddAttribute(HtmlTextWriterAttribute.AutoComplete, "off");
		}

		/// <summary>
		/// Renders the jQuery "Ready Event" script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderReadyScript(ScriptBuilder script) {
			base.RenderReadyScript(script);
			if(ReadOnly && IsEnabled) RenderReadOnlyScript(script);
		}

		/// <summary>
		/// Creates the properties object that will be rendered as the jQuery UI initialization options.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="properties">A <see cref="PropertyCollection"/> object that will become the jQuery UI initialization options.</param>
		protected override void AddInitProperties(PropertyCollection properties) {
			base.AddInitProperties(properties);

			string appendText = AppendText ?? string.Empty;
			if(appendText.Length != 0) properties.AddValue("appendText", appendText, true);

			string buttonImage = ButtonImage ?? string.Empty;
			if(buttonImage.Length != 0) properties.AddValue("buttonImage", ResolveUrl(buttonImage));

			bool buttonImageOnly = ButtonImageOnly;
			if(buttonImageOnly != defaultButtonImageOnly) properties.AddValue("buttonImageOnly", buttonImageOnly);

			string buttonText = ButtonText ?? string.Empty;
			if(buttonText != defaultButtonText) properties.AddValue("buttonText", buttonText, buttonImage.Length == 0);

			string closeText = CloseText ?? string.Empty;
			if(closeText != defaultCloseText) properties.AddValue("closeText", closeText, true);

			bool constrainInput = ConstrainInput;
			if(constrainInput != defaultConstrainInput) properties.AddValue("constrainInput", constrainInput);

			DateTime? defaultDate = DefaultDate;
			if(defaultDate.HasValue) properties.AddValue("defaultDate", defaultDate.Value, false);

			Duration duration = showAnimControl == null ? Duration : showAnimControl.Speed;
			if(duration != defaultDuration) properties.AddValue("duration", showAnimControl.Speed);

			bool goToCurrent = GoToCurrent;
			if(goToCurrent != defaultGoToCurrent) properties.AddValue("gotoCurrent", goToCurrent);

			RelativeValue shortYearCutoff = ShortYearCutoff;
			if(shortYearCutoff != defaultShortYearCutoff) properties.AddValue("shortYearCutoff", shortYearCutoff);

			DatePickerAnimation showAnim = ShowAnim;
			if(showAnimControl != null) properties.AddValue("showAnim", showAnimControl.Effect);
			else if(showAnim != defaultShowAnim) properties.AddValue("showAnim", showAnim);

			DropDownShowOn showOn = ShowOn;
			if(showOn != defaultShowOn) properties.AddEnumValue("showOn", showOn);

			if(showAnimControl != null) {
				PropertyCollection showOptions = showAnimControl.CreateOptionProperties();
				if(showOptions.Count != 0) properties.AddValue("showOptions", showOptions);
			}

			string onClientClose = OnClientClose ?? string.Empty;
			if(onClientClose.Length != 0) properties.AddScript("onClose", onClientClose);

		}

		/// <summary>
		/// Renders client script to create a read-only control when <see cref="ReadOnly"/> is true and <see cref="WebControl.Enabled"/> is true.  This is accomplished by calling the widget's disable method followed by removing the actual input's disabled attribute.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		/// <seealso cref="WebControl.Enabled"/>
		protected virtual void RenderReadOnlyScript(ScriptBuilder script) {

			base.RenderDisableScript(script);

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".removeAttr('disabled');");
		}

		#endregion


		#region Methods: Post data

		/// <summary>
		/// Processes postback data for an ASP.NET server control.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="postDataKey">The key identifier for the control.</param>
		/// <param name="postCollection">The collection of all incoming name values.</param>
		/// <returns>True if the server control's state changes as a result of the postback; otherwise, false.</returns>
		protected override bool LoadPostData(string postDataKey, NameValueCollection postCollection) {

			Page.ClientScript.ValidateEvent(postDataKey);

			DateTime? date = SelectedDate;
			DateTime? newDate = ConstrainDate(ParseDate(postCollection[postDataKey]));

			if(newDate == date) return false;

			SelectedDate = newDate;
			return true;
		}

		/// <summary>
		/// Signals the server control to notify the ASP.NET application that the state of the control has changed.
		/// This method is used primarily by control developers.
		/// </summary>
		protected override void RaisePostDataChangedEvent() {
			OnSelectedDateChanged(EventArgs.Empty);
		}

		#endregion


		#region Methods: Private routines

		private DateTime? ConstrainDate(DateTime? date) {

			if(date == null) return date;

			DateTime? minDate = MinDate;
			if(minDate.HasValue && date < minDate.Value) return null;

			DateTime? maxDate = MaxDate;
			if(maxDate.HasValue && date > maxDate.Value) return null;

			return date;
		}

		#endregion


	}

}